package dreadnought;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

/*
* This class represents a single ship
* Ships have a certain length which is between 1 and 5 units.
* A ship with length n has n blocks.
* Ships have blocks which have the following two states:
* damaged or undamaged with all blocks undamaged at the start.
* Ships have an anchor point which describes its position on the grid.
*/

public class Ship extends JLabel
{
    //the damage state of the ship's blocks
    private boolean[] damage;

    //the orientation of the ship
    private Orientation orientation;

    //the anchor coordinate of the ship
    private GridCoordinate anchor;

    //a reference to the main game object
    private Game game;

    //the type of ship
    private ShipType type;

    //the length of the ship in grid units
    private int length;

    //ship constructor
    public Ship(Game game, ShipType type)
    {
        this.game = game;
        this.type = type;

        this.length = getLengthFromShipType();

        //initalize the block damage array
        this.damage = new boolean[this.length];

        //set all blocks to undamaged (false)
        for (int i=0; i<this.length; i++)
            this.damage[i] = false;
        
        //set default orientation
        this.orientation = Orientation.HORIZONTAL;

        //set the inital anchor position
        setInitialAnchor();

        //set JLabel to be opaque so background color will be drawn
        this.setOpaque(true);

        //set background color
        this.setBackground(game.COLORS_SHIP);

        //set the initial position of the ship onscreen
        setBounds();

        //allow component to receive focus so that it can receive key events
        this.setFocusable(true);

        //add event listeners
        ShipListener listener = new ShipListener();
        this.addMouseListener(listener);
        this.addKeyListener(listener);
    }

    //this sets the inital anchor position of the ship based
    //on the ships type. the ship can be moved around
    //later by the user during deployment
    private void setInitialAnchor()
    {
        int row; //which row the ship will be on
     
        if (this.type == ShipType.PATROLBOAT)
            row = 0;
        else if (this.type == ShipType.SUBMARINE)
            row = 1;
        else if (this.type == ShipType.DESTROYER)
            row = 2;
        else if (this.type == ShipType.BATTLESHIP)
            row = 3;
        else if (this.type == ShipType.CARRIER)
            row = 4;
        else
            throw new OutOfRangeException("invalid ship type");

        this.anchor = new GridCoordinate(0, row);
    }

    //gets the length of this ship in grid units
    //ship length is between 1 and 5 units
    //and is based on the ships type
    private int getLengthFromShipType()
    {
        if (this.type == ShipType.PATROLBOAT)
            return 2;
        else if (this.type == ShipType.SUBMARINE)
            return 3;
        else if (this.type == ShipType.DESTROYER)
            return 3;
        else if (this.type == ShipType.BATTLESHIP)
            return 4;
        else if (this.type == ShipType.CARRIER)
            return 5;
        else
            throw new OutOfRangeException("invalid ship type");
    }

    //returns the type of this ship
    public ShipType getType()
    {
        return this.type;
    }

    //this is just an accessor for this.length
    //getLengthFromShipType must initially be called
    //by the contructor to set this.length
    public int getLength()
    {
        return this.length;
    }

    //sets the orientation of the ship
    public void setOrientation(Orientation orientation)
    {
        this.orientation = orientation;
        this.setBounds();
        this.repaint();
    }

    //gets the orientation of the ship
    public Orientation getOrientation()
    {
        return this.orientation;
    }

    //damages the block with the given block number
    public void inflictDamage(int blockID)
    {
        if (blockID >= this.damage.length || blockID < 0)
            throw new OutOfRangeException("Invalid block ID.");
        this.damage[blockID] = true;
    }

    //toggles the ships orientation
    //ie changes it to vertical if it is currently horizontal and vice versa
    public void toggleOrientation()
    {
        //TODO: add code to only allow orientation to
        //be toggled if the ship will still be within bounds
        //after the orientation is changed.
        //otherwise, do nothing like the move_____() functions
        if (this.orientation == Orientation.HORIZONTAL)
            this.orientation = Orientation.VERTICAL;
        else
            this.orientation = Orientation.HORIZONTAL;

        //validation, keep the ship inside the grid after toggling:
        int newEndCol=this.anchor.getCol() + this.length;
        if (newEndCol>=10 )
            this.anchor.setCol(10-this.length);
        int newEndRow=this.anchor.getRow() + this.length;
        if (newEndRow>=10 )
            this.anchor.setRow(10-this.length);
    }
    
    public void setAnchor(GridCoordinate anchor)
    {
        this.anchor = anchor;
        this.setBounds();
        this.repaint();
    }
    
    public GridCoordinate getAnchor()
    {
        return this.anchor;
    }

    
    //moves this ship up one unit
    //only if it can move up without being out of bounds
    public void moveUp()
    {
        int x = this.anchor.getRow() - 1;
        if (x >= 0)
            this.anchor.setRow(x);
    }
    
    //moves this ship left one unit
    //only if it can move left without being out of bounds
    public void moveLeft()
    {
        int x = this.anchor.getCol() - 1;
        if (x >= 0)
            this.anchor.setCol(x);
    }

    /*
     * Note that moveDown() and moveRight() are more complicated
     * than moveUp() and moveLeft() because moveUp() and moveDown()
     * only have to take into consideration whether the new anchor
     * coordinate would be out of bounds. moveDown() and moveRight()
     * have to take into account the orientation of the ship.
     */

    //moves this ship down one unit
    //only if it can move down without being out of bounds
    public void moveDown()
    {
        //if the orientation is horizontal, determining whether
        //the ship will still be in bounds is simple because
        //we only have to check if the new anchor coordinate
        //is within bounds
        //if the orientation is vertical, we must determine
        //the new southernmost coordinate which will be ship length
        //blocks south of the new anchor coordinate
        
        int newAnchorRow = this.anchor.getRow() + 1;
        if (this.orientation == Orientation.HORIZONTAL)
        {
            if (newAnchorRow < 10)
                this.anchor.setRow(newAnchorRow);
        }
        else
        {
            if (newAnchorRow + this.length  <= 10)
                this.anchor.setRow(newAnchorRow);
        }
    }
    
    //moves this ship right one unit
    //only if it can move right without being out of bounds
    public void moveRight()
    {
        int newAnchorCol = this.anchor.getCol() + 1;

        if (this.orientation == Orientation.VERTICAL)
        {
            if (newAnchorCol < 10)
                this.anchor.setCol(newAnchorCol);
        }
        else
        {
            if (newAnchorCol + this.length  <= 10)
                this.anchor.setCol(newAnchorCol);
        }
    }


    //returns an array of GridCoordinates representing
    //the grid cells occupied by each block of this ship.
    //if a cell of the grid is hit, this method will be
    //called on each ship to determine if the grid cell
    //which was hit was one of the blocks of the ship.
    //the blocks of the ship are calculated based on the
    //ships anchor point and the ship's orientation
    public GridCoordinate[] getBlocks()
    {
        GridCoordinate[] blocks = new GridCoordinate[this.length];
        //the first block is simply the anchor coordinate
        blocks[0] = this.anchor;
        //the other blocks depend on the ships orientation
        int row = this.anchor.getRow();
        int col = this.anchor.getCol();
        for (int i=1; i<this.length; i++)
        {
            if (this.orientation == Orientation.HORIZONTAL)
                blocks[i] = new GridCoordinate(col+i, row);
            else
                blocks[i] = new GridCoordinate(col, row+i);
        }
        return blocks;
    }

    //returns true if any of the blocks of this ship
    //occupy the given GridCoordinate
    public boolean occupiesCoordinate(GridCoordinate g)
    {
        return (this.getBlockId(g) != -1);
    }

    //returns the id of the block of the ship which occupies the
    //given GridCoordinate or -1 if none of the blocks occupy the
    //given GridCoordinate
    public int getBlockId(GridCoordinate g)
    {
        //get the coordinates of the blocks of this ship
        GridCoordinate[] blockCoords = this.getBlocks();

        //check each block coordinate against g
        for (int i=0; i<this.length; i++)
        {
            if (blockCoords[i].equals(g))
                return i;
        }
        return -1;
    }

    //sets the absolute screen position of this ship
    //based on the ships length, orientation and anchor
    //this will be called initially by the contructor
    //then later as the user deploys ships when this ship is moved
    public void setBounds()
    {
        int col = this.anchor.getCol();
        int row = this.anchor.getRow();
        int cell = game.CELL; //the size of each sqare cell
        int pad = game.PAD; //the padding around the grid
        int cellPad = 5; //the padding around the ship inside the cell
        
        if (this.orientation == Orientation.HORIZONTAL)
        {
            this.setBounds(
                    pad+col*cell + cellPad,
                    pad+row*cell + cellPad,
                    cell*length - cellPad*2,
                    cell - cellPad*2);
        }
        else
        {
            this.setBounds(
                    pad+col*cell + cellPad,
                    pad+row*cell + cellPad,
                    cell - cellPad*2,
                    cell*length - cellPad*2);
        }
    }

    public void paintComponent(Graphics g)
    {
        if (game.selectedShip == this)
            this.setBorder(BorderFactory.createLineBorder(game.COLORS_HIGHLIGHT, 3));
        else
            this.setBorder(null);
        
        super.paintComponent(g);
    }

    //returns true if this ship and another ship share any blocks in common
    public boolean overlaps(Ship other)
    {
        GridCoordinate[] thisBlocks = this.getBlocks();
        GridCoordinate[] thatBlocks = other.getBlocks();

        //determine if thisBlocks contains any coordinates which are
        //equal to any of the coordinates in thatBlocks
        for (GridCoordinate i : thisBlocks)
        {
            for (GridCoordinate j : thatBlocks)
            {
                if (i.equals(j))
                    return true;
            }
        }

        return false;
    }

    //returns a reference to this Ship instance
    //for use with the ShipListener private class
    //this is the only way I know to allow ShipListener to
    //retrieve a reference to this.
    public Ship getThis()
    {
        return this;
    }

    //returns true if this ship has been sunk.
    //ie all blocks have been damaged.
    public boolean isSunk()
    {
        for (boolean block : this.damage)
        {
            if (!block)
                return false;
        }
        return true;
    }

    private class ShipListener implements MouseListener, KeyListener
    {
        //when the player clicks a ship, it becomes the selected ship
        //the selected ship is highighted
        //it also receives keyboard events to move it around
        //ships can only be selected during deployment
        public void mouseClicked(MouseEvent e)
        {
            //ships can only be selected during deployment
            if (game.getState() != GameState.DEPLOYMENT)
                return;

            //keep a referenced to the previouly selected ship
            Ship previouslySelectedShip = game.selectedShip;

            //set the selected ship to this one
            game.selectedShip = getThis();

            repaint(); //make this ship repaint itself

            //make the previously selected ship repaint itself
            //but only if the previously selected ship is not null
            if (previouslySelectedShip != null)
                previouslySelectedShip.repaint();

            //give this ship keyboard focus
            requestFocus();
        }
        
        public void keyPressed(KeyEvent e)
        {
            //ships can only be moved during deployment
            if (game.getState() != GameState.DEPLOYMENT)
                return;
            
            if (e.getKeyCode() == KeyEvent.VK_UP)
            {
                moveUp();
            }
            else if (e.getKeyCode() == KeyEvent.VK_DOWN)
            {
                moveDown();
            }
            else if (e.getKeyCode() == KeyEvent.VK_LEFT)
            {
                moveLeft();
            }
            else if (e.getKeyCode() == KeyEvent.VK_RIGHT)
            {
                moveRight();
            }
            else if (e.getKeyCode() == KeyEvent.VK_SPACE)
            {
                toggleOrientation();
            }

            setBounds(); //recalculate the bounds of the ship onscreen
            repaint(); //repaint the screen
        }

        public void mousePressed(MouseEvent e) {}
        public void mouseExited(MouseEvent e) {}
        public void mouseEntered(MouseEvent e) {}
        public void mouseReleased(MouseEvent e) {}
        public void keyTyped(KeyEvent e) {}
        public void keyReleased(KeyEvent e) {}
    }
}